package model;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import com.google.common.collect.HashMultimap;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JOptionPane;

public class Base implements Observable {
	private HashMultimap<Long, Document> dbDocument;
	private HashMultimap<Long, Person> dbPerson;
	private List<Observer> obs = new ArrayList<Observer>();
	private Person loggedUser = null;
	
	public Base() {
		
		// Charger la base depuis les fichiers
		if(!this.restore()) {
			// Si fichiers introuvable, g�n�rer une autre base
			this.dbDocument = HashMultimap.create(1000, 25);
			this.dbPerson = HashMultimap.create(300, 10);
			
			this.addUser(new StaffMember("Admin", "Admin", "admin", "admin"));
			javax.swing.JOptionPane.showMessageDialog(null, "Fichiers de sauvegardes introuvable, création d'une base vide\navec l'utilisateur admin/admin, pensez à changez le mot de passe", "Nouvelle base", JOptionPane.WARNING_MESSAGE); 
	
		}
	}
	
	public boolean isDocumentModifiable(Document doc) {
		return doc.getBorrowBy() == null && doc.getListReserve().isEmpty();
	}
	
	public boolean isUserModifiable(Person p) {
		if(this.loggedUser.equals(p)) {
			return false;
		}
		if(!p.getType().equals("Bibliothécaire")) {
			User u = (User)p;
			return u.getListDocs().isEmpty() && u.getListDocsReserve().isEmpty();
		}
		
		return true;		
	}
	
	public void UserMaj(Person before, Person after) {
		
		// Document emprunter ou réservé
		if(!this.isUserModifiable(before)) {
			return;
		}
		
		this.UserDelete(before);
		this.addUser(after);
	}
	
	public void UserDelete(Person p) {
		// Document empruntés ou réservé ou utilisateur courant
		if(!this.isUserModifiable(p)) {
			return;
		}
		
		long[] partialHash = new long[4];
		partialHash[0] = ((long)this.hashString(p.getName())) << 48;
		partialHash[1] = ((long)this.hashString(p.getFirstName())) << 32;
		partialHash[2] = ((long)this.hashString(p.getPseudo())) << 16;
		partialHash[3] = ((long)this.hashString(p.getType()));

        for(int i=0; i<4; ++i) {
			this.dbPerson.remove(partialHash[i], p);
			this.dbPerson.remove(partialHash[(i+1)%4] | partialHash[(i+2)%4] | partialHash[(i+3)%4], p);
		}
		for(int k=0; k<3; ++k) {
			for(int i=0; i<3-k; ++i) {
				for(int j=1+k; j<4; ++j) {
					this.dbPerson.remove(partialHash[i] | partialHash[j], p);
				}
			}
		}
		this.dbPerson.remove(partialHash[0] | partialHash[1] | partialHash[2] | partialHash[3], p);
	}
	
	public void DocumentMaj(Document before, Document after) {
		
		// Document emprunter ou réservé
		if(!this.isDocumentModifiable(before)) {
			return;
		}
		
		this.DocumentDelete(before);
		this.addDocument(after);
	}
	
	public void DocumentDelete(Document doc) {
		// Document emprunter ou réservé
		if(!this.isDocumentModifiable(doc)) {
			return;
		}
		
		long[] partialHash = new long[4];
		partialHash[0] = ((long)this.hashString(doc.getEditor())) << 48;
		partialHash[1] = ((long)this.hashString(doc.getTitle())) << 32;
		partialHash[2] = ((long)doc.getYear()) << 16;
		partialHash[3] = ((long)this.hashString(doc.getCategory()));
		
		for(int i=0; i<4; ++i) {
			this.dbDocument.remove(partialHash[i], doc);	
			this.dbDocument.remove(partialHash[(i+1)%4] | partialHash[(i+2)%4] | partialHash[(i+3)%4], doc);	
		}
		for(int k=0; k<3; ++k) {
			for(int i=0; i<3-k; ++i) {
				for(int j=1+k; j<4; ++j) {
					this.dbDocument.remove(partialHash[i] | partialHash[j], doc);
				}
			}
		}
		this.dbDocument.remove(partialHash[0] | partialHash[1] | partialHash[2] | partialHash[3], doc);
		
	}
	
	public Set<Document> documentSearch(String editor, String title, Short year,
			String category, List<String> authors, String docType, int docLenght,
			String dayDate) {
		return this.secondaryDocumentSearch(this.primaryDocumentSearch(editor, title, year, category), authors, docType, docLenght, dayDate);
	}

	public Set<Person> personSearch(String name, String firstName, String pseudo, String type) {
		long key = 0L;
		if(name != null && !"".equals(name)) {
			key |= ((long)this.hashString(name) << 48);
		}
		if(firstName != null && !"".equals(firstName)) {
			key |= ((long) this.hashString(firstName) << 32);
		}
		if(pseudo != null && !"".equals(pseudo)) {
			key |= ((long)this.hashString(pseudo) << 16);
		}
		if(type != null && !"".equals(type)) {
			key |= ((long)this.hashString(type));
		}
        if(key == 0L) {
			return new HashSet<Person>(this.dbPerson.values());
		}
		return new HashSet<Person>(this.dbPerson.get(key));
	}
	
	private Set<Document> secondaryDocumentSearch(Set<Document> where, List<String> authors,
			String docType, int docLenght, String dayDate) {
		
		Iterator<Document> it;
		Document doc;
		
		// Auteurs
		if(authors != null && !authors.isEmpty()) {
			it = where.iterator();
			while(it.hasNext()) {
				doc = it.next();
				List<String> docAuthors = doc.getAuthors();
				if(!docAuthors.containsAll(authors)) {
					it.remove();
				}
			}
		}
		
		// docType
		if(docType != null && !"".equals(docType)) {
			it = where.iterator();
			while(it.hasNext()) {
				doc = it.next();
				// test du type
				if(doc.getCategory().equals("Livre") && !((Book)doc).getType().equals(docType)) {
					it.remove();
				}
				else if(doc.getCategory().equals("CD") && !((CD)doc).getType().equals(docType)) {
					it.remove();
				}
				else if(doc.getCategory().equals("DVD") && !((DVD)doc).getType().equals(docType)) {
					it.remove();
				}
			}
		}
		
		// docLenght
		if(docLenght > 0) {
			it = where.iterator();
			while(it.hasNext()) {
				doc = it.next();
				// test du type
				if(doc.getCategory().equals("Livre") && ((Book)doc).getNbPage() != docLenght) {
					it.remove();
				}
				else if(doc.getCategory().equals("CD") && ((CD)doc).getLenght() != docLenght) {
					it.remove();
				}
				else if(doc.getCategory().equals("DVD") && ((DVD)doc).getLenght() != docLenght) {
					it.remove();
				}
				else if(doc.getCategory().equals("Magazine") && ((Magazine)doc).getNbPage() != docLenght) {
					it.remove();
				}
			}
		}
		
		// dayDate
		if(dayDate != null && !"".equals(dayDate)) {
			it = where.iterator();
			while(it.hasNext()) {
				doc = it.next();
				// test du type
				if(doc.getCategory().equals("Journal") && !((NewsPaper)doc).getDayDate().equals(dayDate)) {
					it.remove();
				}
			}
		}
		
		return where;
	}
	
	private Set<Document> primaryDocumentSearch(String editor, String title, Short year, String category) {
		long key = 0L;
		if(editor != null && !"".equals(editor)) {
			key |= ((long) this.hashString(editor) << 48);
		}
		if(title != null && !"".equals(title)) {
			key |= ((long) this.hashString(title) << 32);
		}
		if(year != null) {
			key |= ((long) year << 16);
		}
		if(category != null && !"".equals(category)) {
			key |=((long) this.hashString(category));
		}
		if(key == 0L) {
			return new HashSet<Document>(this.dbDocument.values());
		}
		return new HashSet<Document>(this.dbDocument.get(key));
	}
	
	// Retourne un short différent pour tou String différent passé en paramètre
	private short hashString(String s) {
		double f = Math.PI;
		int l = s.length();
		for(int i=0; i<l; ++i) {
			f += s.charAt(i);
		}
		f /= l;
		f -= 97;
		f *= s.charAt(0);
		if(l>2) {
			f *= s.charAt(l-1);
		}
		s = "" + f;
		String[] ss = s.split("[.]");
		return new Integer(Integer.parseInt((ss[0] + ss[1]).substring(0, 9))).shortValue();
	}
	
	// Réinitialise la base (la vide complètement)
	public void reInit() {
		this.dbDocument.clear();
		this.dbPerson.clear();
	}
	
	// Ajoute un utilisateur
	public void addUser(Person p) {
                
		long[] partialHash = new long[4];
		partialHash[0] = ((long)this.hashString(p.getName())) << 48;
		partialHash[1] = ((long)this.hashString(p.getFirstName())) << 32;
		partialHash[2] = ((long)this.hashString(p.getPseudo())) << 16;
		partialHash[3] = ((long)this.hashString(p.getType()));

        for(int i=0; i<4; ++i) {
			this.dbPerson.put(partialHash[i], p);
			this.dbPerson.put(partialHash[(i+1)%4] | partialHash[(i+2)%4] | partialHash[(i+3)%4], p);
		}
		for(int k=0; k<3; ++k) {
			for(int i=0; i<3-k; ++i) {
				for(int j=1+k; j<4; ++j) {
					this.dbPerson.put(partialHash[i] | partialHash[j], p);
				}
			}
		}
		this.dbPerson.put(partialHash[0] | partialHash[1] | partialHash[2] | partialHash[3], p);
	}
	
	// Renvoi l'user loggé
	public Person getLoggedPerson() {
		return this.loggedUser;
	}
	
	// Ajoute un document
	public void addDocument(Document d) {
		long[] partialHash = new long[4];
		partialHash[0] = ((long)this.hashString(d.getEditor())) << 48;
		partialHash[1] = ((long)this.hashString(d.getTitle())) << 32;
		partialHash[2] = ((long)d.getYear()) << 16;
		partialHash[3] = ((long)this.hashString(d.getCategory()));
		
		for(int i=0; i<4; ++i) {
			this.dbDocument.put(partialHash[i], d);	
			this.dbDocument.put(partialHash[(i+1)%4] | partialHash[(i+2)%4] | partialHash[(i+3)%4], d);	
		}
		for(int k=0; k<3; ++k) {
			for(int i=0; i<3-k; ++i) {
				for(int j=1+k; j<4; ++j) {
					this.dbDocument.put(partialHash[i] | partialHash[j], d);
				}
			}
		}
		this.dbDocument.put(partialHash[0] | partialHash[1] | partialHash[2] | partialHash[3], d);
	}
	
	// Renvoi 0 si l'utilisateur est inconnu (ou pass éronné
	// 1 si c'est un utilisateur
	// 2 si c'est un membre du staff
	public int login(String pseudo, String pass) {
		Set<Person> verif  = personSearch(null, null, pseudo,  null);
		Person p;
		if(verif.isEmpty()) {
			return 0;
		}
		else  {
			p = verif.toArray(new Person[0])[0];
			if(p.getPass().equals(pass)) {
				this.loggedUser = p;
				if(p.getType().equals("Etudiant") || p.getType().equals("Professeur")) {
					return 1;
				}
				else if(p.getType().equals("Bibliothécaire")) {
					return 2;
				}
			}
		}
		return 0;
	}
	
	// Sérialize notre base
	public void serialize() {
		try {
			FileOutputStream fos = new FileOutputStream("db.dat");
	
			// cr�ation d'un "flux objet" avec le flux fichier
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			try {
				// s�rialisation : �criture de l'objet dans le flux de sortie
				oos.writeObject(this.dbDocument);
				oos.writeObject(this.dbPerson);
				// on vide le tampon
				oos.flush();
			}
			finally {
				//fermeture des flux
				try {
					oos.close();
				} finally {
					fos.close();
				}
			}
		}
		catch(IOException e) {
			
		}
	}
	
	// Restore notre base depuis le fichier de sérialization db.dat
	private boolean restore() {
		boolean ret = false;
		try {
			// ouverture d'un flux d'entr�e depuis le fichier "personne.serial"
			FileInputStream fis = new FileInputStream("db.dat");
			// cr�ation d'un "flux objet" avec le flux fichier
			ObjectInputStream ois= new ObjectInputStream(fis);
			try {	
				// d�s�rialisation : lecture de l'objet depuis le flux d'entr�e
				this.dbDocument = (HashMultimap<Long, Document>) ois.readObject();
				this.dbPerson = (HashMultimap<Long, Person>) ois.readObject();
				ret = true;
				
			}
			finally {
				// on ferme les flux
				try {
					ois.close();
				} finally {
					fis.close();
				}
			}
		}
		catch(IOException e) {
			
		}
		catch(ClassNotFoundException e) {
			
		}
		return ret;
	}
	
	// Renvoi les catégories existantes de document.
	public List<String> getCategories() {
		List<String> categories = new ArrayList<String>(10);
		
		categories.add("Livre");
		categories.add("Journal");
		categories.add("Magazine");
		categories.add("CD");
		categories.add("DVD");
		
		return categories;
	}
	
	// Renvoi les catégories d'utilisateurs
	public List<String> getUserTypes() {
		List<String> types = new ArrayList<String>(10);
		
		types.add("Etudiant");
		types.add("Professeur");
		types.add("Bibliothécaire");
		
		return types;
	}

	// Renvoi les documents emprunté par un user
	//(pas de paramètre = prendre ceux de l'user loggé)
	public List<Document> getBorrowedDocument() {
		if(this.loggedUser != null && !this.loggedUser.getType().equals("Bibliothécaire")) {
			return ((User)this.loggedUser).getListDocs();
		}
		return null;
	}
	
	public List<Document> getBorrowedDocument(Person p) {
		if(p != null && !p.getType().equals("Bibliothécaire")) {
			return ((User)p).getListDocs();
		}
		return null;
	}
	
	// Emprunte un document
	public boolean borrow(Document d) {
		if(this.loggedUser != null && !this.loggedUser.getType().equals("Bibliothécaire")) {
			if(((User)this.loggedUser).borrow(d)) {
				List<Object> data = new ArrayList<Object>();
				data.add((User)this.loggedUser);
				data.add(new ArrayList<Document>(((User)this.loggedUser).getListDocs()));
				data.add(new ArrayList<Document>(((User)this.loggedUser).getListDocsReserve()));
				this.updateObservers(data);
				return true;
			}
		}
		return false;
	}
	
	public boolean reserve(Document d) {
		if(this.loggedUser != null && !this.loggedUser.getType().equals("Bibliothécaire")) {
			if(((User)this.loggedUser).reserve(d)) {
				List<Object> data = new ArrayList<Object>();
				data.add((User)this.loggedUser);
				data.add(new ArrayList<Document>(((User)this.loggedUser).getListDocs()));
				data.add(new ArrayList<Document>(((User)this.loggedUser).getListDocsReserve()));
				this.updateObservers(data);
				return true;
			}
		}
		return false;
	}
	
	public boolean bringBack(Document d) {
		if(!this.loggedUser.getType().equals("Bibliothécaire")) {
			d.setBorrowBy(null);
			d.setDateBorrow(null);
			if(d.getListReserve().isEmpty()) {
				d.setDateAvaillable(null);
			}
			if(((User)this.loggedUser).getListDocs().remove(d)) {
				List<Object> data = new ArrayList<Object>();
				data.add((User)this.loggedUser);
				data.add(new ArrayList<Document>(((User)this.loggedUser).getListDocs()));
				data.add(new ArrayList<Document>(((User)this.loggedUser).getListDocsReserve()));
				this.updateObservers(data);
				return true;
			}
		}
		return false;
	}
	
	public boolean noReserve(Document d) {
		if(!this.loggedUser.getType().equals("Bibliothécaire")) {
			if(((User)this.loggedUser).stopReserve(d)) {
				List<Object> data = new ArrayList<Object>();
				data.add((User)this.loggedUser);
				data.add(new ArrayList<Document>(((User)this.loggedUser).getListDocs()));
				data.add(new ArrayList<Document>(((User)this.loggedUser).getListDocsReserve()));
				this.updateObservers(data);
				return true;
			}
		}
		return false;
	}
	
	public boolean isBorrowable(Document d) {
		if(!this.loggedUser.getType().equals("Bibliothécaire")) {
			return ((User)this.loggedUser).canBorrow() && d.isBorrowable((User)this.loggedUser);
		}
		return false;
	}
	
	public boolean isReservable(Document d) {
		if(!this.loggedUser.getType().equals("Bibliothécaire")) {
			return ((User)this.loggedUser).canBorrow() && !((User)this.loggedUser).equals(d.getBorrowBy()) && !d.getListReserve().contains((User)this.loggedUser);
		}
		return false;
	}
	
	public boolean isBorrowed(Document d) {
		if(!this.loggedUser.getType().equals("Bibliothécaire")) {
			return ((User)this.loggedUser).getListDocs().contains(d);
		}
		return false;
	}
	
	public boolean isReserved(Document d) {
		if(!this.loggedUser.getType().equals("Bibliothécaire")) {
			return ((User)this.loggedUser).getListDocsReserve().contains(d);
		}
		return false;	
	}
	
	public void shareDataWithObs() {
		this.shareDataWithObs(this.loggedUser);
	}
	
	public void shareDataWithObs(Person p) {
		if(!p.getType().equals("Bibliothécaire")) {
			List<Object> data = new ArrayList<Object>();
			data.add((User)p);
			data.add(new ArrayList<Document>(((User)p).getListDocs()));
			data.add(new ArrayList<Document>(((User)p).getListDocsReserve()));
			this.updateObservers(data);
		}
	}
	
	public void logout() {
		this.loggedUser = null;
	}

	@Override
	public void addObserver(Observer obs) {
		this.obs.add(obs);
	}

	@Override
	public void updateObservers(Object data) {
		for(Observer obs: this.obs) {
			obs.update(data);
		}
	}

	@Override
	public void delObserver(Observer obs) {
		this.obs.remove(obs);
	}

	public void changePass(String pass) {
		this.loggedUser.setPass(pass);
	}
}
